define(["jQuery", "jsPlumb", "jsPlumbToolkit"], function (jQuery, jsPlumb, jsPlumbToolkit) {
    'use strict';

    function Lineage() {
        //主界面
        var mainElement, canvasElement, miniViewElement, controls, toolkit, renderer, data;
        var nodeMap, edgeMap, edgeSourceMap, edgeTargetMap, isZoomToFit = false;

        function initDataMap(nodes, edges) {
            nodeMap = {};
            nodes.forEach(function (node) {
                nodeMap[node.id] = node;
                node.columns.forEach(function (col) {
                    nodeMap[node.id + "." + col.id] = col;
                })
            });

            edgeMap = {};
            edgeSourceMap = {};
            edgeTargetMap = {};
            edges.forEach(function (edge) {
                edgeMap[edge.source + "-" + edge.target] = edge;

                if (!edgeSourceMap[edge.source]) edgeSourceMap[edge.source] = {};
                edgeSourceMap[edge.source][edge.target] = edge;
                if (!edgeTargetMap[edge.target]) edgeTargetMap[edge.target] = {};
                edgeTargetMap[edge.target][edge.source] = edge;
            });
        }

        function mapForEach(map, callback) {
            if (map) {
                for (var i in map) {
                    callback(map[i], i);
                }
                // for (var i = map.length - 1; i >= 0; i--) {
                //     callback(map[i], i);
                // }
            }
        }

        function findCascadeEdges(nodeId, callback) {
            var tempEdgeMap = {};
            findCascadeSourceEdges(nodeId, tempEdgeMap);
            findCascadeTargetEdges(nodeId, tempEdgeMap);

            mapForEach(tempEdgeMap, function (edge, i) {
                callback(edge, i)
            })
        }

        function computeEdgeId(edge) {
            return edge.source + "-" + edge.target;
        }

        function findCascadeSourceEdges(nodeId, tempMap) {
            // only find column;
            var map = edgeSourceMap[nodeId];
            mapForEach(map, function (edge) {
                var key = computeEdgeId(edge);
                if (!tempMap[key]) {
                    tempMap[key] = edge;
                    findCascadeSourceEdges(edge.target, tempMap);
                }
            });
        }

        function findCascadeTargetEdges(nodeId, tempMap) {
            // only find column;
            var map = edgeTargetMap[nodeId];
            mapForEach(map, function (edge) {
                var key = computeEdgeId(edge);
                if (!tempMap[key]) {
                    tempMap[key] = edge;
                    findCascadeTargetEdges(edge.source, tempMap);
                }
            });
        }

        function initToolkit() {
            var idFunction = function (n) {
                return n.id;
            };

            var typeFunction = function (n) {
                return n.type;
            };

            //画布
            canvasElement = mainElement.querySelector(".canvas");
            //画布右上角的缩略视图，包含在canvasElement中
            // miniViewElement = mainElement.querySelector(".miniview"),
            //画布左上角控制按钮栏，包含在canvasElement中
            controls = mainElement.querySelector(".controls");
            //画布外面的菜单栏，用于拖拽添加新组件
            //  nodePalette = mainElement.querySelector(".node-palette");

            toolkit = jsPlumbToolkit.newInstance({
                idFunction: idFunction,
                typeFunction: typeFunction,
                edgeIdFunction: function (data) {
                    return computeEdgeId(data);
                },
                nodeFactory: function (type, data, callback) {
                    data.columns = [];
                    jsPlumbToolkit.Dialogs.show({
                        id: "dlgName",//对应 erMapComponents.html中的div元素
                        title: "Enter " + type + " name:",
                        onOK: function (d) {
                            data.name = d.name; //div中jtk-att="name
                            if (data.name) {
                                if (data.name.length >= 2) {
                                    data.id = data.name.replace(" ", "_").toLowerCase();
                                    callback(data);
                                }
                                else
                                    alert(type + " names must be at least 2 characters!");
                            }
                        }
                    });

                },
                edgeFactory: function (params, data, callback) {
                    // you must hit the callback if you provide the edgeFactory.
                    callback(data);
                    // unless you want to return false, to abandon the edge
                    //return false;
                },
                portFactory: function (params, data, callback) {
                    params.node.data.columns.push(data);
                    callback(data);
                },
                //
                // For a given Node, return the parts of its dataset that we want to configure as Ports.
                // This is called when the data is being loaded. It is an optional argument to the newInstance
                // method.
                portExtractor: function (data, node) {
                    return data.columns || [];
                }
                ,
                // 连接目标前调用的方法（不是连线开始前），判断source与target是否为同一个节点，如果是同一个节点就不能连线
                beforeConnect: function (source, target) {
                    return false;//return source !== target && source.getNode() !== target.getNode();
                },
                //连线开始之前调用的方法，如果为false将不允许拉出线
                beforeStartConnect: function (source, target) {
                    return true;
                }
            });

            return {
                toolkit: toolkit,
                canvasElement: canvasElement,
                // miniViewElement: miniViewElement,
                controls: controls
                // nodePalette: nodePalette
            };
        }

        function dragAndDropNewTablesOrViews() {
            // ------------------------ drag and drop new tables/views -----------------

            //
            // Here, we are registering elements that we will want to drop onto the workspace and have
            // the toolkit recognise them as new nodes.
            //
            //  typeExtractor: this function takes an element and returns to jsPlumb the type of node represented by
            //                 that element. In this application, that information is stored in the 'jtk-node-type' attribute.
            //
            //  dataGenerator: this function takes a node type and returns some default data for that node type.
            //
            renderer.registerDroppableNodes({
                droppables: nodePalette.querySelectorAll("li"),
                dragOptions: {
                    zIndex: 50000,
                    cursor: "move",
                    clone: true
                },
                typeExtractor: function (el, eventInfo, isNativeDrag, eventLocation) {
                    return el.getAttribute("jtk-node-type");
                },
                dataGenerator: function (type, draggedElement, eventInfo, eventLocation) {
                    return {name: type};
                }
            });
        }

        function appendHtmlElements() {
            var html = '<div class="canvas autoHeight" style="border:none">' +
                '<div class="controls">' +
                '<i class="fa fa-reply go-back-button" title="返回"></i>' +
                '<i class="fa fa-home" reset title="缩放到适合大小"></i>';
            mainElement.innerHTML = "";
            mainElement.innerHTML = html;
        };

        function autoHeight() {
            $(function () {
                var _jahDivs = $(".autoHeight");  //初始化高度
                _jahDivs.height($(window).height() - 80);
                //当文档窗口发生改变时 触发
                $(window).resize(function () {
                    _jahDivs.height($(window).height() - 80);
                }).resize();
            })
        };

        this.initTableLineage = function (_) {
            this.setData(_);
            appendHtmlElements();

            jsPlumbToolkit.ready(function () {
                var __ret = initToolkit();
                var canvasElement = __ret.canvasElement;
                // var miniViewElement = __ret.miniViewElement;
                var controls = __ret.controls;
                // var nodePalette = __ret.nodePalette;

                jsPlumbToolkit.Dialogs.initialize({
                    selector: ".dlg"
                });
                // Instruct the toolkit to render to the 'canvas' element. We pass in a model of nodes, edges and ports, which
                // together define the look and feel and behaviour of this renderer.  Note that we can have 0 - N renderers
                // assigned to one instance of the Toolkit..
                renderer = toolkit.render({
                    container: canvasElement,
                    view: {
                        // Two node types - 'table' and 'view'
                        nodes: {
                            "table": {
                                template: "tmplTableLineage",
                                events: {
                                    //tap:tapFn
                                }
                            },
                            "view": {
                                template: "tmplView"
                            }
                        },
                        // 定义edge的样式
                        edges: {
                            "common": {
                                anchor: ["Left", "Right"], // anchors for the endpoints
                                /*connector: "StateMachine",  //  画直线*/
                                Connector: ["StateMachine", {curviness: 10}], //浮线
                                // connector: ["Flowchart", { cornerRadius: 5 } ], //画折线
                                cssClass: "common-edge",// link-animation
                                events: {
                                    "dbltap": function (params) {
                                        _editEdge(params.edge);
                                    }
                                },
                                overlays: [
                                    ["Label", {
                                        label: function (params) {
                                            var source = params.component.edge.source.data;
                                            var target = params.component.edge.target.data;
                                            var edgeId = source.parent + "." + source.id + "-"
                                                + target.parent + "." + target.id;

                                            if (edgeMap[edgeId].name == "" || edgeMap[edgeId].name == null)
                                                return "<i class='label-hide'></i>";
                                            else
                                                return "<i title='" + edgeMap[edgeId].remark + "'>"
                                                    + edgeMap[edgeId].name + "</i>";
                                        }
                                    }],
                                    ["Arrow", {fillStyle: "#89bcde", width: 10, length: 10, location: 1}]
                                ]
                            }
                        },
                        // There is only one type of Port - a column - so we use the key 'default' for the port type
                        // Here we define the appearance of this port,
                        // and we instruct the Toolkit what sort of Edge to create when the user drags a new connection
                        // from an instance of this port. Note that we here we tell the Toolkit to create an Edge of type
                        // 'common' because we don't know the cardinality of a relationship when the user is dragging. Once
                        // a new relationship has been established we can ask the user for the cardinality and update the
                        // model accordingly.
                        ports: {
                            "default": {
                                template: "tmplColumn",
                                paintStyle: {fillStyle: "#f76258"},		// the endpoint's appearance
                                hoverPaintStyle: {fillStyle: "#434343"}, // appearance when mouse hovering on endpoint or connection
                                edgeType: "common", // 连线的样式，是上面 edge定义的样式，如果不填或者没有找到就用默认的
                                maxConnections: -1, //不限制连线条数，原因是数据库表字段可能为对个表的外键
                                dropOptions: {  //drop options for the port. here we attach a css class.
                                    hoverClass: "drop-hover"
                                },
                                allowLoopback: false,   // do not allow loopback connections from a port to itself.
                                allowNodeLoopback: false, // do not allow connections from this port to any other port on the same node.
                                interceptors: {//拦截器
                                    //在port（column）连线到target的时候触发
                                    beforeDrop: function (params) {
                                        return false;//不允许添加连线
                                    },
                                    //删除连线的时候触发
                                    beforeDetach: function (params) {
                                        return false;//不允许删除连线
                                    }
                                }
                            }
                        }
                    },
                    // Layout the nodes using a 'Spring' (force directed) layout. This is the best layout in the jsPlumbToolkit
                    // for an application such as this.
                    layout: {
                        type: "Spring",
                        parameters: {
                            padding: [150, 150]
                        }
                    },
                    //右上方的小视图窗（可拖拽定位的缩略图）
                    // miniview: {
                    //     container: miniViewElement
                    // },
                    // Register for certain events from the renderer. Here we have subscribed to the 'nodeRendered' event,
                    // which is fired each time a new node is rendered.  We attach listeners to the 'new column' button
                    // in each table node.  'data' has 'node' and 'el' as properties: node is the underlying node data,
                    // and el is the DOM element. We also attach listeners to all of the columns.
                    // At this point we can use our underlying library to attach event listeners etc.
                    events: {
                        // This is called by the Toolkit when a new Port is added to a Node. In this application, that occurs
                        // when the user adds a new column to a table. It is instigated by the application code preparing the
                        // JS data for a new column, and then calling toolkit.portAdded(node, portData); Note that the
                        // application also adds the data to the backing model itself.
                        // In this application, the portElement was rendered by the 'tmplColumn' template, and it is an LI.
                        // the nodeElement was rendered by 'tmplTable', and it has a UL inside of it to which we want to attach
                        // the column's LI.
                        portAdded: function (params) {
                            //params.nodeEl.querySelectorAll("ul")[0].appendChild(params.portEl);
                        },
                        edgeAdded: function (params) {
                            // Check here that the edge was not added programmatically, ie. on load.
                            // if (params.addedByMouse) {
                            //     _editEdge(params.edge, true);
                            // }
                        },
                        canvasClick: function (e) {
                            toolkit.clearSelection();
                        }
                    },
                    jsPlumb: {
                        Endpoint: "Blank"
                    },
                    dragOptions: {
                        // filter: ".name .node,.jtk-node,.table-column div"
                        filter: "i, .view .name span, .table1 .name span, .table-column *"
                    },
                    consumeRightClick: false,
                    zoomToFit: isZoomToFit
                });

                // listener for mode change on renderer.
                renderer.bind("modeChanged", function (mode) {
                    jsPlumb.removeClass(controls.querySelectorAll("[mode]"), "selected-mode");
                    jsPlumb.addClass(controls.querySelectorAll("[mode='" + mode + "']"), "selected-mode");
                });

                // pan mode/select mode
                jsPlumb.on(controls, "tap", "[mode]", function () {
                    renderer.setMode(this.getAttribute("mode"));
                });

                // on home button click, zoom content to fit.
                jsPlumb.on(controls, "tap", "[reset]", function () {
                    toolkit.clearSelection();
                    renderer.zoomToFit();
                });

                jsPlumb.on(canvasElement, "mouseover", ".table-column span", function (params) {
                    var table = $(params.target).parent().parent().parent().parent();
                    var tableId = table.data("jtkNodeId");
                    var col = $(params.target).parent().parent();
                    var colName = col.data("portId");

                    findCascadeEdges(tableId + "." + colName, function (edge) {
                        var sourceNodeId = edge.source.split(".")[0];
                        var sourcePortId = edge.source.split(".")[1];
                        var targetNodeId = edge.target.split(".")[0];
                        var targetPortId = edge.target.split(".")[1];
                        $("[data-jtk-node-id='" + sourceNodeId + "']")
                            .find("[data-port-id='" + sourcePortId + "']").css("opacity", "0.8");
                        $("[data-jtk-node-id='" + targetNodeId + "']")
                            .find("[data-port-id='" + targetPortId + "']").css("opacity", "0.8");
                    });
                });

                jsPlumb.on(canvasElement, "mouseout", ".table-column span", function (event) {
                    $(".table-column").css("opacity", "1");
                });

                // dragAndDropNewTablesOrViews();

                autoHeight();

                toolkit.load(
                    {data: data}
                );
                //refreshCss(true);

                $(".label-hide").parent().addClass("hide");// 连线文字为空的时候，隐藏文字的父Div。
            });

            return this;
        };

        this.setMainElement = function (_) {
            mainElement = _;
            return this;
        };

        this.setData = function (_) {
            if (_) {
                data = _;
                initDataMap(data.nodes, data.edges);
            }
            return this;
        };

        this.layout = function (layoutType) {
            var intervalWidth = 450;
            var intervalHeight = 20;
            var tableTitleHeight = 36;
            var colHeight = 26
            var startX = 200;
            var startY = 100;

            function getEdgeId(col) {
                return col.parent + "." + col.id;
            }

            function calculateTableHeight(table) {
                return tableTitleHeight + table.columns.length * colHeight + intervalHeight;
            }

            function calculateTableLevel() {
                // Find left tables
                var tables = [];
                data.nodes.forEach(function (d) {
                    var isEdgeTarget = false;
                    d.columns.forEach(function (col) {
                        if (edgeTargetMap[getEdgeId(col)] != undefined) {
                            isEdgeTarget = true;
                            return;
                        }
                    })

                    if (!isEdgeTarget) {
                        tables.push(d);
                        d.level = 0;
                    }
                });

                function findNext(table) {
                    table.columns.forEach(function (col) {
                        if (edgeSourceMap[getEdgeId(col)] != undefined) {
                            var map = edgeSourceMap[getEdgeId(col)];
                            mapForEach(map, function (d) {
                                var targetParentId = nodeMap[d.target].parent;
                                var parent = nodeMap[targetParentId];
                                if (parent != undefined && parent.level == -1) {
                                    parent.level = table.level + 1;
                                    if (table.nextTables == undefined) table.nextTables = [];
                                    table.nextTables.push(parent);
                                    findNext(parent);
                                }
                            })
                        }
                    })
                }

                // Find next tables
                tables.forEach(function (table) {
                    findNext(table);
                });

                console.log(tables);
                return tables;
            }

            function layoutNormal() {
                var nodes = data.nodes;
                var edges = data.edges;
                var length = nodes.length;
                var colNum = Math.ceil(Math.sqrt(length));

                switch (length) {
                    case 2:
                        nodes[0].left = startX;
                        nodes[0].top = startY;
                        nodes[1].left = startX + intervalWidth;
                        nodes[1].top = startY;
                        break;
                    case 3:
                        var longer = (nodes[0].columns.length >= nodes[1].columns.length
                            ? nodes[0].columns.length : nodes[1].columns.length);
                        nodes[0].left = startX;
                        nodes[0].top = startY;
                        nodes[1].left = startX + intervalWidth * 2;
                        nodes[1].top = startY;
                        nodes[2].left = startX + intervalWidth;
                        nodes[2].top = startY + intervalHeight * longer;
                        break;
                    default:
                        var i = 0, rowIndex = 0, colIndex = 0, x = startX, y = startY;
                        while (i < length) {
                            nodes[i].left = x + intervalWidth * rowIndex;//x
                            nodes[i].top = y;//y

                            console.log("i " + i + " colIndex " + colIndex + " rowIndex " + rowIndex
                                + " left " + nodes[i].left + " top " + nodes[i].top);
                            if (colIndex < colNum - 1) {
                                y += calculateTableHeight(nodes[i]);
                                colIndex++;
                            } else {
                                y = startY;
                                colIndex = 0;
                                rowIndex++;
                            }
                            i++;
                        }
                }
            }

            function layoutLeftToRight() {
                var position = [];

                function initLevelPosition(level) {
                    position.length <= level + 1 && position.push({left: startX + intervalWidth * level , top: startY});
                }

                function movePosition(level, left, top) {
                    position[level].left += left;
                    position[level].top += top;
                }

                function nextTable(table) {
                    if (table != undefined && table.nextTables != undefined) {
                        initLevelPosition(table.level + 1);
                        table.nextTables.forEach(function (t) {
                            t.left = position[t.level].left;
                            t.top = position[t.level].top;
                            movePosition(t.level, 0, calculateTableHeight(t));
                        });

                        table.nextTables.forEach(function (t) {
                            nextTable(t);
                        });
                    }
                }

                var x = startX, y = startY;
                calculateTableLevel().forEach(function (table) {
                    initLevelPosition(table.level);
                    table.left = position[table.level].left;
                    table.top = position[table.level].top;
                    nextTable(table);
                    movePosition(table.level, 0, calculateTableHeight(table));
                });
            }


            if (layoutType == undefined) layoutType = "normal";
            switch (layoutType) {
                case "normal":
                    layoutNormal();
                    break;
                case "leftToRight":
                    layoutLeftToRight();
                    break;
            }

            if (length > 10) isZoomToFit = true;

            return this;
        };

        return this;
    }

    var factory = {};

    function createLineage(mainElement) {
        return new Lineage().setMainElement(mainElement);
    }

    factory.createLineage = createLineage;

    return factory;
});